﻿using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Threading;
using System.Threading.Tasks;
using BTM_Client_DBwork.Constants;
using Client_Server_Interface;
using DevControl = DevicesInt.DeviceControl;
using IDeviceInterface = Client_Server_Interface.IDeviceInterface;

namespace BTM_Client_Application.SupportClasses
{
    public class DeviceWorker : IMonitoringDeviceWorker, IDisposable
    {
        //private List<DeviceTreeItem.DeviceInfo> _devList;
        private static event NewErrorRecordDelegate NewErrorRecordEvent;
        private bool _initialized;
        private bool _started;
        private static int _statusRequestPeriod = 1;
        private CancellationTokenSource _cts;
        private CancellationToken _ct;
        private TaskFactory _tf;
        private Task[] _tasks;

        protected static void OnNewErrorRecordEvent(DeviceTreeItem.DeviceInfo device, ObservableCollection<ErrorListRecord> records, int unreadCount, int totalUnreadCount)
        {
            NewErrorRecordDelegate handler = NewErrorRecordEvent;
            if (handler != null) handler(device, records, unreadCount, totalUnreadCount);
        }

        //public static Dictionary<DeviceTreeItem.DeviceInfo, List<ErrorListRecord>> DevicesState { get; set; }
        public List<IDeviceInterface> ActiveDeviceList { get; set; }

        public static List<IDeviceInterface> CreateDeviceList(DeviceTree devTree)
        {
            var devList = (from region in devTree.DeviceTreeItems
                           from station in region.SubItems
                           from device in station.SubItems
                           select device.AdditionalInfo as DeviceTreeItem.DeviceInfo).ToList();
            //в _devList нет данных по регионам
            var activeDeviceList = new List<IDeviceInterface>();
            foreach (var deviceInfo in devList)
            {
                if (string.IsNullOrEmpty(deviceInfo.Title))
                    deviceInfo.Title = deviceInfo.Device;
                
                String devLibName = deviceInfo.LibraryName;
                try
                {
                    var tLibName = AliveInfo.DBwork.GetLibraryName(deviceInfo.Device);
                    if (!string.IsNullOrEmpty(tLibName))
                        devLibName = tLibName;
                }
                catch (Exception)
                {
                    AliveInfo.DBwork.SetLibraryName(deviceInfo.Device, devLibName);
                }
                deviceInfo.LibraryName = devLibName;
                var hw = new HardWorker(devLibName);
                try
                {
                    hw.Initialize(deviceInfo.IpAddress);
                    if (deviceInfo.IsActive)
                        activeDeviceList.Add(new Device(hw.GetStatus, deviceInfo, OnNewErrorRecordEvent));
                }
                catch (ClientException ex)
                {
                    //if(ex.Message != ClientExceptionMessage.FILE_IS_NOT_EXIST)
                    AliveInfo.ClientEvents.AddEvent(ex.Message, ex.AdditionalInfo);
                }
                catch (Exception ex)
                {
                    AliveInfo.ClientEvents.AddEvent(
                        Res.Rm.GetString(ClientExceptionMessage.LIBRARY_INITIALIZATION_ERROR, AliveInfo.CurrentCulture),
                        ex.Message + "\r\n" + devLibName);
                }
            }
            return activeDeviceList;
        }

        public void Initialize(List<IDeviceInterface> activeDeviceList, NewErrorRecordDelegate newErrorRecordReaction, string requestPeriod)
        {
            if (_initialized)
            {
                return;
            }
                
            int statusRequestPeriod;
            if (!int.TryParse(requestPeriod, out statusRequestPeriod))
            {
                IncorrectInputValueException.Throw("integer status request period", requestPeriod);
            }
            _statusRequestPeriod = statusRequestPeriod*1000;
            NewErrorRecordEvent = null;
            NewErrorRecordEvent += newErrorRecordReaction;
            _initialized = true;
            ActiveDeviceList = activeDeviceList;
        }

        public static DevicesInt.DeviceControl GetControl(string libName, string ip)
        {
            var hw = new HardWorker(libName);
            hw.Initialize(ip);
            return hw.GetControl();
        }

        public void ChangeDeviceOpenedState(DeviceTreeItem.DeviceInfo deviceInfo, bool opened)
        {
            var dev = ActiveDeviceList.Single(d => d.GetHashCode() == deviceInfo.GetHashCode());
            dev.Opened = opened;
        }

        public bool AllOpenedDevicesState
        {
            set
            {
                if (value)
                {
                    OnNewErrorRecordEvent(null, null, 0, 0);
                    foreach (var deviceInterface in ActiveDeviceList)
                    {
                        deviceInterface.UnreadCount = 0;
                    }
                }
                DeviceBlank.AllOpened = value;
            }
        }

        public void Start()
        {
            if(!_initialized)
                ClientException.Throw(ClientExceptionMessage.NOT_INITIALIZED);
            if(_started)
                ClientException.Throw(ClientExceptionMessage.ALREADY_STARTED);
            _cts = new CancellationTokenSource();
            _ct = _cts.Token;
            _tf = new TaskFactory();
            _tasks = new Task[ActiveDeviceList.Count];
            int i = 0;
            foreach (var device in ActiveDeviceList)
            {
                var device1 = device;
                _tasks[i++] = _tf.StartNew(()=>device1.Action(_ct), _ct);
            }
            _started = true;
        }

        public void Stop()
        {
            if(!_started)
                return;
            _cts.Cancel();
            try
            {
                Task.WaitAll(_tasks, 2000, _ct);
            }
            catch (OperationCanceledException)
            {
                DeviceBlank.TotalUnreadRecordsCount = 0;
                OnNewErrorRecordEvent(null, null, 0, 0);
                _started = false;
                _initialized = false;
            }
        }
        
        /// <summary>
        /// Работает со всеми активными устройствами. Проводит опрос и заполняет списки с ошибками.
        /// </summary>
        public class Device : DeviceBlank
        {

            public Device(GetStatusDelegate getStatus, DeviceTreeItem.DeviceInfo deviceInfo, NewErrorRecordDelegate errorDelegate)
            {
                GetStatus = getStatus;
                DevInfo = deviceInfo;
                OnErrorAppear = errorDelegate;
                UnreadCount = 0;
                Opened = false;
            }

            /// <summary>
            /// Вся работа по проверке статусов устройств.
            /// </summary>
            /// <param name="ct">Токен отмены.</param>
            public override void Action(CancellationToken ct)
            {
                while(true)
                {
                    if (ct.IsCancellationRequested)
                        break;
                    string status;
                    try
                    {
                        status = GetStatus();
                    }
                    catch (Exception ex)
                    {
                        if (ex is TargetInvocationException && ex.InnerException != null)
                            AliveInfo.ClientEvents.AddEvent(Res.Rm.GetString(ClientExceptionMessage.LIBRARY_INITIALIZATION_ERROR, AliveInfo.CurrentCulture),
                                                            ex.InnerException.Message);
                        return;
                    }
                    
                    CreateErrorsList(status);
                    OnErrorAppear(DevInfo, ErrorsList, UnreadCount, TotalUnreadRecordsCount);
                    OnErrorsListChanged();
                    int period;
                    if(!int.TryParse(AliveInfo.DBwork.GetParameterValue(OptionNames.STATUS_REQUEST_PERIOD), out period))
                        Thread.Sleep(_statusRequestPeriod);
                    else
                        Thread.Sleep(period*1000);
                }
            }

            /// <summary>
            /// Разбивает строку от устройства на список для заполнения datagrid.
            /// </summary>
            /// <param name="status">Строка с ошибками от устройтва.</param>
            /// <returns></returns>
            protected override void CreateErrorsList(string status)
            {
                int added = 0;
                bool allAdded = ErrorsList == null || ErrorsList.Count < 1;
                //Делим на ошибки
                var records = status.Split(';');
                if (!records.Any())
                    ErrorsList = new ObservableCollection<ErrorListRecord>();
                var list = new ObservableCollection<ErrorListRecord>();
                //Все записи с прошлого цикла помечаются как старые
                if(ErrorsList!=null)
                    foreach (var errorListRecord in ErrorsList)
                    {
                        if (errorListRecord.State != ErrorListRecord.RecordState.Delete)
                            errorListRecord.State = ErrorListRecord.RecordState.Old;
                    }
                //Перебор всех записей, полученных с устройства
                foreach (var record in records)
                {
                    if (record == "")
                        continue;
                    if (record == "ok")
                    {
                        break;
                    }
                    //Если ok то сплитить нечего!!!!!!!!
                    var split = record.Split(':');
                    var severity = split[0];        //Приоритет (info, warning, alarm, err)
                    var info = split[1];
                    if (info == DevicesInt.Errors.NO_CONNECTION)
                        info = Res.Rm.GetString("NoConnectionWithDevice");
                    var newError = new ErrorListRecord
                        {
                            Date = DateTime.Now,
                            Device = DevInfo.Device,
                            Info = info,
                            Region = DevInfo.Region,
                            Station = DevInfo.ParentInfo.Station,
                            Status = severity
                        };
                    if (severity.Contains("err"))
                    {
                        newError.Status = "Alarm";
                    }
                    if (allAdded)
                    {
                        newError.State = ErrorListRecord.RecordState.Add;
                        added++;
                    }
                    else
                    {
                        IEnumerable<ErrorListRecord> t = ErrorsList.Where(e => e.GetRecordId() == newError.GetRecordId()).ToList();
                        //Если такой же записи нет, то текущая добавляется
                        if (!t.Any())
                        {
                            newError.State = ErrorListRecord.RecordState.Add;
                            added++;
                        }
                        else
                        {
                            ErrorListRecord prevDev = t.Single();
                            //Если запись есть, но на прошлом цикле была удалена, то добавляется снова
                            if (prevDev.State == ErrorListRecord.RecordState.Delete)
                            {
                                newError.State = ErrorListRecord.RecordState.Add;
                                added++;
                            }
                                //Если такая же запись есть, то ничего не происходит
                            else
                            {
                                prevDev.State = ErrorListRecord.RecordState.Seen;
                                newError = prevDev;
                            }
                        }
                    }
                    //Запись добавляется в обновленный список
                    list.Add(newError);
                }
                if(!allAdded)
                    //Среди записей, оставшихся с прошлого цикла ищутся те, которые нужно удалить из списка и помечаются
                    foreach (var error in ErrorsList)
                    {
                        if (error.State != ErrorListRecord.RecordState.Seen && error.State != ErrorListRecord.RecordState.Delete)
                        {
                            error.State = ErrorListRecord.RecordState.Delete;
                            list.Add(error);
                            if(UnreadCount > 0)
                                added--;
                        }
                    }
                //Старый список заменяется обновленным
                ErrorsList =  list;
                if (AllOpened)
                    TotalUnreadRecordsCount = 0;
                else if (!Opened)
                {
                    TotalUnreadRecordsCount -= UnreadCount;
                    UnreadCount += added;
                    TotalUnreadRecordsCount += UnreadCount;
                }
            }
        }

        /// <summary>
        /// Используется для работы с библиотеками для каждого устройства.
        /// </summary>
        private class HardWorker
        {
            private bool _initialized;
            private DynamicFunc _dynamicFunc;
            //private readonly string _dllName;
            private readonly string _dllPath;
            private const string LibsPath = "\\Libs\\";
            private string _devIp;
            private DevicesInt.IDeviceInterface _deviceInterface;

            public HardWorker(string libName)
            {
                //_dllName = libName;
                _dllPath = AliveInfo.CurDir + LibsPath + libName;
            }

            /// <summary>
            /// Получение строки состояни, списка текущих ошибок на устройстве.
            /// </summary>
            /// <returns></returns>
            public string GetStatus()
            {
                if (!_initialized)
                {
                    ClientException.Throw(ClientExceptionMessage.NOT_INITIALIZED);
                }
                if (_deviceInterface != null)
                {
                    return _deviceInterface.Getstatus(_devIp);
                }
                string[] parsString = { _devIp };
                var args = new object[2];
                args[0] = "getstatus";
                args[1] = parsString;
                return _dynamicFunc(args);
            }

            public DevicesInt.DeviceControl GetControl()
            {
                if (!_initialized)
                {
                    ClientException.Throw(ClientExceptionMessage.NOT_INITIALIZED);
                }
                if (_deviceInterface == null)
                    return null;
                var cont = _deviceInterface.GetController(_devIp);
                return cont;
            }

            public void Initialize(string ipAddress)
            {
                if(_initialized)
                    return;
                if (!File.Exists(_dllPath))
                {
                    ClientException.Throw(ClientExceptionMessage.FILE_IS_NOT_EXIST, _dllPath);
                }
                if (!Res.CheckIp(ipAddress))
                {
                    IncorrectInputValueException.Throw(ClientExceptionMessage.INCORRECT_IP_ADDRESS, ipAddress);
                }
                _devIp = ipAddress;
                Assembly assemblyDll = Assembly.LoadFile(_dllPath);
                Type[] typeslist = assemblyDll.GetExportedTypes();
                Type useType = null;
                //MethodInfo[] methods = typeslist[0].GetMethods();
                MethodInfo callMethod = null;
                foreach (var oneType in typeslist)
                {
                    var methods = oneType.GetMethods();
                    callMethod = methods.FirstOrDefault(met => met.Name.Contains("CallMethod"));
                    if (callMethod != null)
                    {
                        useType = oneType;
                        break;
                    }
                    //callMethod = methods.First(met => met.Name.Contains("CallMethod"));
                }
                if (callMethod == null)
                    ClientException.Throw("Method in device library not found. " + assemblyDll.GetName());
                //callMethod = methods.First(met => met.Name.Contains("CallMethod"));
                var callClass = Activator.CreateInstance(useType);
                _deviceInterface = callClass as DevicesInt.IDeviceInterface;
                _dynamicFunc = MakeDynamicFunc(callClass, callMethod);
                _initialized = true;
            }

            private delegate string DynamicFunc(params object[] parameters);

            private static DynamicFunc MakeDynamicFunc(object target, MethodInfo method)
            {
                return par => (string)method.Invoke(target, par);
            }
        }

        public void Dispose()
        {
            Stop();
        }
    }
}
